package com.kordar.rbac;

import lombok.SneakyThrows;
import org.springframework.beans.BeanUtils;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;

import java.util.*;
import java.util.stream.Collectors;

public abstract class BaseManager implements AuthManager {

    /**
     * @var array a list of role names that are assigned to every user automatically without calling [[assign()]].
     * Note that these roles are applied to users, regardless of their state of authentication.
     */
    protected HashMap<String, Role> defaultRoles = new HashMap<>();

    /**
     * Returns the named auth item.
     *
     * @param name String the auth item name.
     * @return Item the auth item corresponding to the specified name. Null is returned if no such item.
     */
    abstract protected Item getItem(String name);

    /**
     * Returns the items of the specified type.
     *
     * @param type int the auth item type (either [[Item::TYPE_ROLE]] or [[Item::TYPE_PERMISSION]]
     * @return Item[] the auth items of the specified type.
     */
    abstract protected Set<Item> getItems(Integer type);

    /**
     * Adds an auth item to the RBAC system.
     *
     * @param item the item to add
     * @return bool whether the auth item is successfully added to the system.
     */
    abstract protected boolean addItem(Item item);

    /**
     * Adds a rule to the RBAC system.
     *
     * @param rule the rule to add
     * @return bool whether the rule is successfully added to the system.
     */
    abstract protected boolean addRule(Rule rule);

    /**
     * Remove an auth item from the RBAC system.
     *
     * @param item the item to remove
     * @return bool whether the role or permission is successfully removed.
     */
    abstract protected boolean removeItem(Item item);

    /**
     * Remove a rule from the RBAC system.
     *
     * @param rule the rule to remove
     * @return bool whether the rule is successfully removed.
     */
    abstract protected boolean removeRule(Rule rule);

    /**
     * Updates an auth item in the RBAC system
     *
     * @param name String name the name of the item being updated
     * @param item the updated item
     * @return bool whether the auth item is successfully updated.
     */
    abstract protected boolean updateItem(String name, Item item);

    /**
     * Updated a rule to the RBAC system.
     *
     * @param name the name of the rule being updated
     * @param rule the updated rule
     * @return bool whether the rule is successfully updated.
     */
    abstract protected boolean updateRule(String name, Rule rule);

    /**
     * {@inheritDoc}
     */
    public Role createRole(String name) {
        Role role = new Role();
        role.setName(name);
        role.setCreateTime(new Date());
        role.setUpdateTime(new Date());
        return role;
    }

    /**
     * {@inheritDoc}
     */
    public Permission createPermission(String name) {
        Permission permission = new Permission();
        permission.setName(name);
        permission.setCreateTime(new Date());
        permission.setUpdateTime(new Date());
        return permission;
    }

    /**
     * {@inheritDoc}
     */
    public boolean add(Item object) {
        // TODO if the rule of the object is not alive in the system, then to create it to the system
        checkRuleExits(object.getRuleName());
        return addItem(object);
    }

    @SneakyThrows
    protected void checkRuleExits(String ruleName) {
        if (StringUtils.hasLength(ruleName) && getRule(ruleName) == null) {
            Rule rule = (Rule) ClassUtils.forName(ruleName, null).newInstance();
            rule.setName(ruleName);
            addRule(rule);
        }
    }

    /**
     * {@inheritDoc}
     */
    public boolean add(Rule rule) {
        return addRule(rule);
    }

    /**
     * {@inheritDoc}
     */
    public boolean remove(Item object) {
        return removeItem(object);
    }

    /**
     * {@inheritDoc}
     */
    public boolean remove(Rule rule) {
        return removeRule(rule);
    }

    /**
     * {@inheritDoc}
     */
    public boolean update(String name, Item object) {
        // TODO if the rule of the object is not alive in the system, then to create it to the system
        checkRuleExits(object.getRuleName());
        return updateItem(name, object);
    }

    /**
     * {@inheritDoc}
     */
    public boolean update(String name, Rule rule) {
        return updateRule(name, rule);
    }

    /**
     * {@inheritDoc}
     */
    public Role getRole(String name) {
        Role role = new Role();
        BeanUtils.copyProperties(getItem(name), role, "type");
        return role;
    }

    /**
     * {@inheritDoc}
     */
    public Permission getPermission(String name) {
        Permission permission = new Permission();
        BeanUtils.copyProperties(getItem(name), permission, "type");
        return permission;
    }

    public Set<Role> getRoles() {
        Set<Role> roles = new HashSet<Role>();
        getItems(ItemEnum.TYPE_ROLE.getCode()).forEach(e -> {
            Role role = new Role();
            BeanUtils.copyProperties(e, role);
            roles.add(role);
        });
        return roles;
    }

    /**
     * Set default roles
     *
     * @param roles roles in collection "set"
     */
    public void setDefaultRoles(Set<Role> roles) {
        roles.forEach(e-> defaultRoles.put(e.getName(), e));
    }

    /**
     * Set default role
     *
     * @param role one by one of role
     */
    public void setDefaultRoles(Role... role) {
        Arrays.asList(role).forEach(e -> defaultRoles.put(e.getName(), e));
    }

    /**
     * Get default roles
     *
     * @return collection that contain default roles
     */
    public Set<String> getDefaultRoles() {
        return defaultRoles.keySet();
    }

    /**
     * Returns defaultRoles as array of Role objects.
     *
     * @return Role[] default roles. The array is indexed by the role names
     * @since 2.0.12
     */
    public Map<String, Role> getDefaultRoleInstances() {
        return defaultRoles;
    }

    /**
     * {@inheritDoc}
     */
    public Set<Permission> getPermissions() {
        Set<Permission> permissions = new HashSet<>();
        getItems(ItemEnum.TYPE_PERMISSION.getCode()).forEach(e -> {
            Permission permission = createPermission("");
            BeanUtils.copyProperties(e, permission);
            permissions.add(permission);
        });
        return permissions;
    }

    /**
     * Executes the rule associated with the specified auth item.
     * <p>
     * If the item does not specify a rule, this method will return true. Otherwise, it will
     * return the value of [[Rule::execute()]].
     *
     * @param user   string|int the user ID. This should be either an integer or a string representing
     *               the unique identifier of a user. See [[\yii\web\User::id]].
     * @param item   Item $ the auth item that needs to execute its rule
     * @param params array $ parameters passed to [[CheckAccessInterface::checkAccess()]] and will be passed to the rule
     * @return bool the return value of [[Rule::execute()]]. If the auth item does not specify a rule, true will be returned.
     * @throws RuntimeException if the auth item has an invalid rule.
     */
    protected boolean executeRule(Long user, Item item, Map<String, Object> params) {
        if (!StringUtils.hasText(item.getRuleName())) {
            return true;
        }

        Rule rule = getRule(item.getRuleName());
        if (rule != null) {
            return rule.getExecutor().execute(user, item, params);
        }

        throw new RuntimeException("Rule not found: " + item.getRuleName());
    }

    /**
     * Checks whether array of $assignments is empty and [[defaultRoles]] property is empty as well.
     *
     * @param assignments Assignment[] array of user's assignments
     * @return bool whether array of $assignments is empty and [[defaultRoles]] property is empty as well
     * @since 2.0.11
     */
    protected boolean hasNoAssignments(Map<String, Assignment> assignments) {
        return assignments.isEmpty() && defaultRoles.isEmpty();
    }

}
